package moe.util;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Map.Entry;

public class JSONBuilder {
	public static interface JSONable {
		public Map<String, Object> toJSON();
	}

	private Map<String, Object> root;
	private Stack<Object> stack;
	private String callback;

	private JSONBuilder(Map<String, Object> root) {
		this.root = root;
		stack = new Stack<Object>();
		stack.push(root);
	}

	public static JSONBuilder build() {
		return new JSONBuilder(new HashMap<String, Object>());
	}

	public static JSONBuilder builder(Map root) {
		return new JSONBuilder(root);
	}

	public JSONBuilder callback(String callback) {
		this.callback = callback;
		return this;
	}

	/*
	 * selector:like key1.key2.key3 etc.
	 */
	public JSONBuilder select(String selector) {
		String[] keys = selector.split(".");
		Stack<Object> newStack = new Stack<Object>();
		Object current = root;
		newStack.push(current);

		for (int i = 0, length = keys.length; i < length; i++) {
			if (isList(current)) {
				try {
					current = ((List) current).get(Integer.valueOf(keys[i]));
				} catch (IndexOutOfBoundsException e) {
					throw new IndexOutOfBoundsException("Index out of bounds: " + selector
							+ " in item " + i);
				}
			} else {
				current = ((Map) current).get(keys[i]);
			}
			if (!isMap(current) && (!isList(current) || (i != length - 1 && isInt(keys[i + 1])))) {
				throw new IllegalStateException(
						"stack top element isn't a Map(JSON Object) or List(JSON Array)");
			}
			newStack.push(current);
		}
		stack = newStack;
		return this;
	}

	public JSONBuilder end() {
		if (stack.size() <= 1) {
			throw new IllegalStateException("stack size <= 1");
		}
		stack.pop();
		return this;
	}

	public JSONBuilder add(String key, Map value, boolean select) {
		add(key, value);
		if (select) {
			stack.push(value);
		}
		return this;
	}

	public JSONBuilder add(Map value, boolean select) {
		add(value);
		if (select) {
			stack.push(value);
		}
		return this;
	}

	public JSONBuilder add(String key, JSONable value, boolean select) {
		Map<String, Object> json = value.toJSON();
		add(key, json);
		if (select) {
			stack.push(json);
		}
		return this;
	}

	public JSONBuilder add(JSONable value, boolean select) {
		Map<String, Object> json = value.toJSON();
		add(json);
		if (select) {
			stack.push(json);
		}
		return this;
	}

	public JSONBuilder add(String key, Object value, boolean select) {
		if (value == null) {
			add(key, value);
			return this;
		}
		Map<String, Object> map = null;
		Field[] fields = value.getClass().getDeclaredFields();
		if (fields.length > 0) {
			map = new HashMap<String, Object>();
		}
		for (Field field : fields) {
			field.setAccessible(true);
			try {
				map.put(field.getName(), field.get(value));
			} catch (Exception e) {
				continue;
			}
		}

		add(key, map);
		if (select) {
			stack.push(map);
		}
		return this;
	}

	public JSONBuilder add(Object value, boolean select) {
		Map<String, Object> map = null;
		Field[] fields = value.getClass().getDeclaredFields();
		if (fields.length > 0) {
			map = new HashMap<String, Object>();
		}
		for (Field field : fields) {
			field.setAccessible(true);
			try {
				map.put(field.getName(), field.get(value));
			} catch (Exception e) {
				continue;
			}
		}

		add(map);
		if (select) {
			stack.push(map);
		}
		return this;
	}

	public JSONBuilder add(String key, List value, boolean select) {
		add(key, value);
		if (select) {
			stack.push(value);
		}
		return this;
	}

	public JSONBuilder add(List value, boolean select) {
		add(value);
		if (select) {
			stack.push(value);
		}
		return this;
	}

	public JSONBuilder add(String key, Object[] value, boolean select) {
		List list = new ArrayList(Arrays.asList(value));
		add(key, list);
		if (select) {
			stack.push(list);
		}
		return this;
	}

	public JSONBuilder add(Object[] value, boolean select) {
		List list = new ArrayList(Arrays.asList(value));
		add(list);
		if (select) {
			stack.push(list);
		}
		return this;
	}

	public JSONBuilder add(String key, String value) {
		return add(key, (Object) value);
	}

	public JSONBuilder add(String value) {
		return add((Object) value);
	}

	public JSONBuilder add(String key, Number value) {
		return add(key, (Object) value);
	}

	public JSONBuilder add(Number value) {
		return add((Object) value);
	}

	public JSONBuilder add(String key, boolean value) {
		return add(key, (Object) value);
	}

	public JSONBuilder add(boolean value) {
		return add((Object) value);
	}

	public Map<String, Object> getRoot() {
		return this.root;
	}

	public String buildString() {
		return buildString(root);
	}

	private String buildString(Object obj) {
		StringBuilder result = new StringBuilder();
		if (callback != null) {
			result.append(callback).append("(");
		}
		if (isMap(obj)) {
			result.append("{");
			Iterator<Entry<String, Object>> iterator =
					((Map<String, Object>) obj).entrySet().iterator();
			while (iterator.hasNext()) {
				Entry<String, Object> entry = iterator.next();
				String key = entry.getKey().replaceAll("(\")|(\\\\)", "\\\\$1$2");
				String value = toString(entry.getValue());
				result.append("\"").append(key).append("\":").append(value);
				if (iterator.hasNext()) {
					result.append(", ");
				}
			}
			result.append("}");
		} else if (isList(obj)) {
			result.append("[");
			Iterator iterator = ((List) obj).iterator();
			while (iterator.hasNext()) {
				result.append(toString(iterator.next()));
				if (iterator.hasNext()) {
					result.append(", ");
				}
			}
			result.append("]");
		} else {
			result.append("{");
			Field[] fields = obj.getClass().getDeclaredFields();
			Iterator<Field> iterator = Arrays.asList(fields).iterator();
			while (iterator.hasNext()) {
				Field field = iterator.next();
				field.setAccessible(true);
				String key = field.getName().replaceAll("(\")|(\\\\)", "\\\\$1$2");
				String value = null;
				try {
					value = toString(field.get(obj));
				} catch (Exception e) {
					continue;
				}
				result.append("\"").append(key).append("\":").append(value);
				if (iterator.hasNext()) {
					result.append(", ");
				}
			}
			result.append("}");
		}
		if (callback != null) {
			result.append(")");
		}
		return result.toString();
	}

	private String toString(Object obj) {
		String result = null;
		if (obj == null) {
			result = "null";
		} else if (isNumber(obj) || isBoolean(obj)) {
			result = obj.toString();
		} else if (isString(obj)) {
			result = "\"" + obj.toString().replaceAll("(\")|(\\\\)", "\\\\$1$2") + "\"";
		} else if (isMap(obj) || isList(obj)) {
			result = buildString(obj);
		} else {
			result = buildString(obj);
		}
		return result;
	}

	private JSONBuilder add(String key, Object value) {
		Object current = stack.peek();
		if (!isMap(current)) {
			throw new IllegalStateException("current element isn't a Map(JSON Object)");
		}
		((Map) current).put(key, value);
		return this;
	}

	private JSONBuilder add(Object value) {
		Object current = stack.peek();
		if (!isList(current)) {
			throw new IllegalStateException("current element isn't a List(JSON Array)");
		}
		((List) current).add(value);
		return this;
	}

	private boolean isMap(Object obj) {
		return obj instanceof Map;
	}

	private boolean isList(Object obj) {
		return obj instanceof List;
	}

	private boolean isString(Object obj) {
		return obj instanceof String;
	}

	private boolean isNumber(Object obj) {
		return obj instanceof Number;
	}

	private boolean isBoolean(Object obj) {
		return obj instanceof Boolean;
	}

	private boolean isInt(String str) {
		return str.matches("^\\d|[1-9]\\d*$");
	}
}
